from bisect import insort_right
from time import time, sleep
import threading

from khronos.utils.misc import callable_name


class Event(object):
    """An event executes a given callback function when its date is reach in the calendar. Events 
    are created through the Calendar.schedule() method."""
    def __init__(self, calendar, date, callback, priority=0.0, owner=None):
        self.calendar  = calendar
        self.date      = date
        self.callback  = callback
        self.priority  = priority
        self.owner     = owner
        self.activated = False
        
    def __cmp__(self, event):
        return cmp((self.date, self.priority), (event.date, event.priority))
        
    def __repr__(self):
        callback = "<NO_CALLBACK>" if self.callback is None else callable_name(self.callback)
        owner = "" if self.owner is None else ("%s:" % self.owner)
        return ("<%s at %s >> %s%s() [P=%.02f, A=%s] (%08x)>" % 
                (type(self).__name__, self.date, owner, callback, 
                 self.priority, self.activated, id(self)))
        
    def schedule(self):
        self.activated = False
        self.calendar._insert(self)
        
    def cancel(self):
        self.calendar._remove(self)
        
    def activate(self):
        assert not self.activated
        self.activated = True
        self.callback()
        
        
class Calendar(object):
    """Base class for calendar types, providing basic facilities for scheduling events and 
    managing time."""
    def __init__(self, time=0.0):
        self.time = time
        self.events = list()
        
    def clear(self, time=0.0):
        self.time = time
        self.events = list()
        
    def set_time(self, time):
        if time < self.time:
            raise ValueError("invalid attempt to set time to past date")
        while len(self.events) > 0:
            next = self.events[0]
            if next.date > time:
                break
            self.time = next.date
            self.events.pop(0)
            print "[%10s] %s" % (self.time, event)
            next.activate()
        self.time = time
        
    def schedule(self, date, callback, priority=0.0, owner=None):
        event = Event(self, date, callback, priority, owner)
        event.schedule()
        return event
        
    def schedule_after(self, delta, callback, priority=0.0, owner=None):
        return self.schedule(self.time + delta, callback, priority, owner)
        
    def _insert(self, event):
        if event.date < self.time:
            raise ValueError("unable to schedule event (date has already passed)")
        insort_right(self.events, event)
        
    def _remove(self, event):
        self.events.remove(event)
        
        
class RealTimeCalendar(Calendar):
    """A calendar type that can execute events in (approximate) real time or modified time (using 
    a multiplier). It uses an auxiliary thread that sleeps() in fixed time steps and advances 
    time in the calendar at each iteration, until pause() is called or a time limit is reached."""
    def __init__(self, speed=1.0):
        Calendar.__init__(self)
        self.speed = speed
        self.limit = None
        self.running = False
        self.interrupt_run = threading.Event()
        
    def clear(self):
        if self.running:
            raise Exception("cannot clear() running calendar")
        Calendar.clear(self)
        
    def _insert(self, event):
        if self.running and (len(self.events) == 0 or event.date < self.events[0].date):
            self.interrupt_run.set()
        Calendar._insert(self, event)
        
    def run(self, delta=None, limit=None):
        if self.running:
            raise ValueError("calendar is already running")
        if delta is not None and limit is not None:
            self.limit = min(limit, self.time + delta)
        elif limit is not None:
            self.limit = limit
        elif delta is not None:
            self.limit = self.time + delta
        else:
            self.limit = None
        threading.Thread(target=self._run).start()
        
    def pause(self):
        if self.running:
            self.running = False
            self.stop_running.set()
            
    def _run(self):
        self.running = True
        while self.running and (self.limit is None or self.time < self.limit):
            # compute the time until the next event (if no event is scheduled, default to 60 t.u.)
            timeout = 60.0
            if len(self.events) > 0:
                timeout = float(self.events[0].date - self.time)
            # if a limit is defined, apply it
            if self.limit is not None:
                timeout = min(timeout, self.limit - self.time)
            # scale the timeout according to the calendar's speed
            timeout /= self.speed
            # wait until the time passes or the interrupt_run event is set
            t0 = time()
            self.interrupt_run.wait(timeout)
            if self.interrupt_run.is_set():
                self.interrupt_run.clear()
            t1 = time()
            self.set_time(self.time + (t1 - t0) * self.speed)
        self.running = False
        
        
def test_scheduling(n=20):
    from random import random, randint
    from functools import partial
    
    calendar = RealTimeCalendar()
    
    def foo(event):
        print "%s: %s" % (calendar.time, event)
        next_date = calendar.events[0].date
        new_event_date = calendar.time + random()
        if new_event_date < next_date:
            print "new next event date %s -> %s" % (next_date, new_event_date)
        new_event = calendar.schedule(new_event_date, None)
        new_event.callback = partial(foo, new_event)
        
    # set up event streams
    for _ in xrange(2):
        event = calendar.schedule_after(random(), None)
        event.callback = partial(foo, event)
    return calendar
    